package com.nageoffer.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.date.Week;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.text.StrBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nageoffer.shortlink.project.common.convention.exception.ClientException;
import com.nageoffer.shortlink.project.common.convention.exception.ServiceException;
import com.nageoffer.shortlink.project.common.enums.VailDateTypeEnum;
import com.nageoffer.shortlink.project.config.GotoDomainWhiteListConfiguration;
import com.nageoffer.shortlink.project.dao.entity.LinkAccessLogsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkAccessStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkBrowserStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkDeviceStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkLocaleStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkNetworkStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkOsStatsDO;
import com.nageoffer.shortlink.project.dao.entity.LinkStatsTodayDO;
import com.nageoffer.shortlink.project.dao.entity.ShortLinkDO;
import com.nageoffer.shortlink.project.dao.entity.ShortLinkGotoDO;
import com.nageoffer.shortlink.project.dao.mapper.LinkAccessLogsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkAccessStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkBrowserStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkDeviceStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkLocaleStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkNetworkStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkOsStatsMapper;
import com.nageoffer.shortlink.project.dao.mapper.LinkStatsTodayMapper;
import com.nageoffer.shortlink.project.dao.mapper.ShortLinkGotoMapper;
import com.nageoffer.shortlink.project.dao.mapper.ShortLinkMapper;
import com.nageoffer.shortlink.project.dto.biz.ShortLinkStatsRecordDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkBatchCreateReqDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkCreateReqDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.nageoffer.shortlink.project.dto.req.ShortLinkUpdateReqDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkBaseInfoRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkBatchCreateRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkCreateRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkGroupCountQueryRespDTO;
import com.nageoffer.shortlink.project.dto.resp.ShortLinkPageRespDTO;
import com.nageoffer.shortlink.project.mq.producer.DelayShortLinkStatsProducer;
import com.nageoffer.shortlink.project.service.LinkStatsTodayService;
import com.nageoffer.shortlink.project.service.ShortLinkService;
import com.nageoffer.shortlink.project.toolkit.HashUtil;
import com.nageoffer.shortlink.project.toolkit.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

import static com.nageoffer.shortlink.project.common.constant.RedisKeyConstant.GOTO_IS_NULL_SHORT_LINK_KEY;
import static com.nageoffer.shortlink.project.common.constant.RedisKeyConstant.GOTO_SHORT_LINK_KEY;
import static com.nageoffer.shortlink.project.common.constant.RedisKeyConstant.LOCK_GID_UPDATE_KEY;
import static com.nageoffer.shortlink.project.common.constant.RedisKeyConstant.LOCK_GOTO_SHORT_LINK_KEY;
import static com.nageoffer.shortlink.project.common.constant.ShortLinkConstant.AMAP_REMOTE_URL;

/**
 * 短链接接口实现层
 * 公众号：马丁玩编程，回复：加群，添加马哥微信（备注：link）获取项目资料
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {

    // 布隆过滤器，用于防止短链接重复创建
    private final RBloomFilter<String> shortUriCreateCachePenetrationBloomFilter;
    // 短链接跳转表Mapper
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    // Redis操作模板
    private final StringRedisTemplate stringRedisTemplate;
    // Redisson客户端，用于分布式锁
    private final RedissonClient redissonClient;
    // 以下为各统计维度的Mapper
    private final LinkAccessStatsMapper linkAccessStatsMapper;
    private final LinkLocaleStatsMapper linkLocaleStatsMapper;
    private final LinkOsStatsMapper linkOsStatsMapper;
    private final LinkBrowserStatsMapper linkBrowserStatsMapper;
    private final LinkAccessLogsMapper linkAccessLogsMapper;
    private final LinkDeviceStatsMapper linkDeviceStatsMapper;
    private final LinkNetworkStatsMapper linkNetworkStatsMapper;
    private final LinkStatsTodayMapper linkStatsTodayMapper;
    private final LinkStatsTodayService linkStatsTodayService;
    private final DelayShortLinkStatsProducer delayShortLinkStatsProducer;
    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;

    // 高德地图API密钥，用于IP定位
    @Value("${short-link.stats.locale.amap-key}")
    private String statsLocaleAmapKey;

    @Value("${short-link.domain.default}")
    private String createShortLinkDefaultDomain;

    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO requestParam) {


        verificationWhitelist(requestParam.getOriginUrl());

        // 生成短链接后缀
        String shortLinkSuffix = generateSuffix(requestParam);
        // 拼接完整短链接URL
        // 旧版本  String fullShortUrl = StrBuilder.create(requestParam.getDomain())
        String fullShortUrl = StrBuilder.create(createShortLinkDefaultDomain)
                .append("/")
                .append(shortLinkSuffix)
                .toString();
        // 构建短链接DO对象
        ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                .domain(createShortLinkDefaultDomain)
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .createdType(requestParam.getCreatedType())
                .validDateType(requestParam.getValidDateType())
                .validDate(requestParam.getValidDate())
                .describe(requestParam.getDescribe())
                .shortUri(shortLinkSuffix)
                .enableStatus(0)
                .totalPv(0)
                .totalUv(0)
                .totalUip(0) //初始化统计访问为0
                .delTime(0L)
                .fullShortUrl(fullShortUrl)
                .favicon(getFavicon(requestParam.getOriginUrl()))
                .build();
        // 构建跳转关系DO对象
        ShortLinkGotoDO linkGotoDO = ShortLinkGotoDO.builder()
                .fullShortUrl(fullShortUrl)
                .gid(requestParam.getGid())
                .build();
        try {
            // 插入短链接和跳转关系
            baseMapper.insert(shortLinkDO);
            shortLinkGotoMapper.insert(linkGotoDO);
        } catch (DuplicateKeyException ex) {
            // 处理短链接重复的情况
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl);
            ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);
            if (hasShortLinkDO != null) {
                log.warn("短链接：{} 重复入库", fullShortUrl);
                throw new ServiceException("短链接生成重复");
            }
        }
        // 缓存短链接到Redis
        stringRedisTemplate.opsForValue().set(
                String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                requestParam.getOriginUrl(),
                LinkUtil.getLinkCacheValidTime(requestParam.getValidDate()), TimeUnit.MILLISECONDS
        );
        // 将短链接添加到布隆过滤器
        shortUriCreateCachePenetrationBloomFilter.add(fullShortUrl);
        // 返回创建结果
        return ShortLinkCreateRespDTO.builder()
                .fullShortUrl("http://" + shortLinkDO.getFullShortUrl())
                .originUrl(requestParam.getOriginUrl())
                .gid(requestParam.getGid())
                .build();
    }
    @Override
    public ShortLinkBatchCreateRespDTO batchCreateShortLink(ShortLinkBatchCreateReqDTO requestParam) {
        // 获取批量请求中的原始URL列表和描述列表
        List<String> originUrls = requestParam.getOriginUrls();
        List<String> describes = requestParam.getDescribes();

        // 初始化结果列表，用于存储每个短链接的创建结果
        List<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();

        // 遍历每个原始URL和对应的描述
        for (int i = 0; i < originUrls.size(); i++) {
            // 将批量请求参数转换为单个短链接创建请求DTO
            // 注意：这里会自动拷贝同名且类型兼容的字段（gid, createdType, validDateType, validDate）
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(requestParam, ShortLinkCreateReqDTO.class);

            // 手动设置无法自动拷贝的字段：单个URL和描述
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));

            try {
                // 调用创建单个短链接的服务方法
                ShortLinkCreateRespDTO shortLink = createShortLink(shortLinkCreateReqDTO);

                // 构建单个短链接的基础信息响应DTO
                ShortLinkBaseInfoRespDTO linkBaseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(shortLink.getFullShortUrl())  // 完整短链接
                        .originUrl(shortLink.getOriginUrl())       // 原始链接
                        .describe(describes.get(i))                // 描述信息
                        .build();

                // 将创建成功的短链接信息添加到结果列表
                result.add(linkBaseInfoRespDTO);
            } catch (Throwable ex) {
                // 捕获并记录创建失败的异常，但不中断整个批量操作
                log.error("批量创建短链接失败，原始参数：{}", originUrls.get(i));
            }
        }

        // 构建最终的批量创建响应DTO
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())      // 成功创建的总数
                .baseLinkInfos(result)     // 成功创建的短链接详情列表
                .build();
    }

    // 更新短链接方法（带事务管理）
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO requestParam) {
        verificationWhitelist(requestParam.getOriginUrl());
        // 构建查询条件：根据原分组ID、完整短链接、未删除标志和启用状态查询
        LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                .eq(ShortLinkDO::getGid, requestParam.getOriginGid())
                .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                .eq(ShortLinkDO::getDelFlag, 0)
                .eq(ShortLinkDO::getEnableStatus, 0);
        // 执行查询
        ShortLinkDO hasShortLinkDO = baseMapper.selectOne(queryWrapper);

        // 校验短链接是否存在
        if (hasShortLinkDO == null) {
            throw new ClientException("短链接记录不存在");
        }

        //==================
        //=================
        // 情况1：分组ID未变更
        if (Objects.equals(hasShortLinkDO.getGid(), requestParam.getGid())) {
            // 构建更新条件：根据完整短链接、新分组ID、未删除标志和启用状态更新
            LambdaUpdateWrapper<ShortLinkDO> updateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                    .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                    .eq(ShortLinkDO::getGid, requestParam.getGid())
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0)
                    // 特殊处理永久有效期类型
                    .set(Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()), ShortLinkDO::getValidDate, null);

            // 构建更新实体（仅更新必要字段）
            ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                    .domain(hasShortLinkDO.getDomain())
                    .shortUri(hasShortLinkDO.getShortUri())
                    .favicon(hasShortLinkDO.getFavicon())
                    .createdType(hasShortLinkDO.getCreatedType())
                    .gid(requestParam.getGid())
                    .originUrl(requestParam.getOriginUrl())
                    .describe(requestParam.getDescribe())
                    .validDateType(requestParam.getValidDateType())
                    .validDate(requestParam.getValidDate())
                    .build();
            // 执行更新
            baseMapper.update(shortLinkDO, updateWrapper);
        }
        // 情况2：分组ID变更
        else {
            // 获取分布式写锁（防止并发修改）
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, requestParam.getFullShortUrl()));
            RLock rLock = readWriteLock.writeLock();

            // 尝试获取锁（非阻塞）
            if (!rLock.tryLock()) {
                throw new ServiceException("短链接正在被访问，请稍后再试...");
            }
            try {
                // 步骤1：逻辑删除旧分组记录
                LambdaUpdateWrapper<ShortLinkDO> linkUpdateWrapper = Wrappers.lambdaUpdate(ShortLinkDO.class)
                        .eq(ShortLinkDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkDO::getGid, hasShortLinkDO.getGid())
                        .eq(ShortLinkDO::getDelFlag, 0)
                        .eq(ShortLinkDO::getDelTime, 0L)
                        .eq(ShortLinkDO::getEnableStatus, 0);
                ShortLinkDO delShortLinkDO = ShortLinkDO.builder()
                        .delTime(System.currentTimeMillis())
                        .build();
                delShortLinkDO.setDelFlag(1);
                baseMapper.update(delShortLinkDO, linkUpdateWrapper);

                // 步骤2：插入新分组记录
                ShortLinkDO shortLinkDO = ShortLinkDO.builder()
                        .domain(createShortLinkDefaultDomain)
                        .originUrl(requestParam.getOriginUrl())
                        .gid(requestParam.getGid())
                        .createdType(hasShortLinkDO.getCreatedType())
                        .validDateType(requestParam.getValidDateType())
                        .validDate(requestParam.getValidDate())
                        .describe(requestParam.getDescribe())
                        .shortUri(hasShortLinkDO.getShortUri())
                        .enableStatus(hasShortLinkDO.getEnableStatus())
                        .totalPv(hasShortLinkDO.getTotalPv())
                        .totalUv(hasShortLinkDO.getTotalUv())
                        .totalUip(hasShortLinkDO.getTotalUip())
                        .fullShortUrl(hasShortLinkDO.getFullShortUrl())
                        .favicon(getFavicon(requestParam.getOriginUrl()))
                        .delTime(0L)
                        .build();
                baseMapper.insert(shortLinkDO);

                // 步骤3：迁移今日统计数据
                LambdaQueryWrapper<LinkStatsTodayDO> statsTodayQueryWrapper = Wrappers.lambdaQuery(LinkStatsTodayDO.class)
                        .eq(LinkStatsTodayDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkStatsTodayDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkStatsTodayDO::getDelFlag, 0);
                List<LinkStatsTodayDO> linkStatsTodayDOList = linkStatsTodayMapper.selectList(statsTodayQueryWrapper);
                if (CollUtil.isNotEmpty(linkStatsTodayDOList)) {
                    // 先删除旧记录
                    linkStatsTodayMapper.deleteBatchIds(linkStatsTodayDOList.stream()
                            .map(LinkStatsTodayDO::getId)
                            .toList()
                    );
                    // 更新分组ID后重新插入
                    linkStatsTodayDOList.forEach(each -> each.setGid(requestParam.getGid()));
                    linkStatsTodayService.saveBatch(linkStatsTodayDOList);
                }

                // 步骤4：更新路由表
                LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                        .eq(ShortLinkGotoDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(ShortLinkGotoDO::getGid, hasShortLinkDO.getGid());
                ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
                shortLinkGotoMapper.deleteById(shortLinkGotoDO.getId());
                shortLinkGotoDO.setGid(requestParam.getGid());
                shortLinkGotoMapper.insert(shortLinkGotoDO);

                // 步骤5：更新各维度统计表（共7个统计表）
                // 5.1 访问基础统计
                LambdaUpdateWrapper<LinkAccessStatsDO> linkAccessStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessStatsDO.class)
                        .eq(LinkAccessStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessStatsDO::getDelFlag, 0);
                LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessStatsMapper.update(linkAccessStatsDO, linkAccessStatsUpdateWrapper);

                // 5.2 地理位置统计
                LambdaUpdateWrapper<LinkLocaleStatsDO> linkLocaleStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkLocaleStatsDO.class)
                        .eq(LinkLocaleStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkLocaleStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkLocaleStatsDO::getDelFlag, 0);
                LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkLocaleStatsMapper.update(linkLocaleStatsDO, linkLocaleStatsUpdateWrapper);

                // 5.3 操作系统统计
                LambdaUpdateWrapper<LinkOsStatsDO> linkOsStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkOsStatsDO.class)
                        .eq(LinkOsStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkOsStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkOsStatsDO::getDelFlag, 0);
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkOsStatsMapper.update(linkOsStatsDO, linkOsStatsUpdateWrapper);

                // 5.4 浏览器统计
                LambdaUpdateWrapper<LinkBrowserStatsDO> linkBrowserStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkBrowserStatsDO.class)
                        .eq(LinkBrowserStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkBrowserStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkBrowserStatsDO::getDelFlag, 0);
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkBrowserStatsMapper.update(linkBrowserStatsDO, linkBrowserStatsUpdateWrapper);

                // 5.5 设备统计
                LambdaUpdateWrapper<LinkDeviceStatsDO> linkDeviceStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkDeviceStatsDO.class)
                        .eq(LinkDeviceStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkDeviceStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkDeviceStatsDO::getDelFlag, 0);
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkDeviceStatsMapper.update(linkDeviceStatsDO, linkDeviceStatsUpdateWrapper);

                // 5.6 网络统计
                LambdaUpdateWrapper<LinkNetworkStatsDO> linkNetworkStatsUpdateWrapper = Wrappers.lambdaUpdate(LinkNetworkStatsDO.class)
                        .eq(LinkNetworkStatsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkNetworkStatsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkNetworkStatsDO::getDelFlag, 0);
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkNetworkStatsMapper.update(linkNetworkStatsDO, linkNetworkStatsUpdateWrapper);

                // 5.7 访问日志
                LambdaUpdateWrapper<LinkAccessLogsDO> linkAccessLogsUpdateWrapper = Wrappers.lambdaUpdate(LinkAccessLogsDO.class)
                        .eq(LinkAccessLogsDO::getFullShortUrl, requestParam.getFullShortUrl())
                        .eq(LinkAccessLogsDO::getGid, hasShortLinkDO.getGid())
                        .eq(LinkAccessLogsDO::getDelFlag, 0);
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .gid(requestParam.getGid())
                        .build();
                linkAccessLogsMapper.update(linkAccessLogsDO, linkAccessLogsUpdateWrapper);
            } finally {
                // 确保锁释放
                rLock.unlock();
            }
        }

        // 处理有效期变更时的缓存更新
        if (!Objects.equals(hasShortLinkDO.getValidDateType(), requestParam.getValidDateType())
                || !Objects.equals(hasShortLinkDO.getValidDate(), requestParam.getValidDate())) {
            // 删除原始链接缓存
            stringRedisTemplate.delete(String.format(GOTO_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
            // 特殊处理过期链接
            if (hasShortLinkDO.getValidDate() != null && hasShortLinkDO.getValidDate().before(new Date())) {
                if (Objects.equals(requestParam.getValidDateType(), VailDateTypeEnum.PERMANENT.getType()) || requestParam.getValidDate().after(new Date())) {
                    // 删除空值标记缓存
                    stringRedisTemplate.delete(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, requestParam.getFullShortUrl()));
                }
            }
        }
    }

    //=============================================
    //===============================================
    @Override
    public IPage<ShortLinkPageRespDTO> pageShortLink(ShortLinkPageReqDTO requestParam) {
        IPage<ShortLinkDO> resultPage = baseMapper.pageLink(requestParam);
        return resultPage.convert(each -> {
            ShortLinkPageRespDTO result = BeanUtil.toBean(each, ShortLinkPageRespDTO.class);
            result.setDomain("http://" + result.getDomain());
            return result;
        });
    }



    // 查询分组内短链接数量的实现方法
    @Override
    public List<ShortLinkGroupCountQueryRespDTO> listGroupShortLinkCount(List<String> requestParam) {
        // 构建MyBatis-Plus查询条件构造器
        // 说明：使用Wrappers.query构建条件，指定返回字段和分组条件
        QueryWrapper<ShortLinkDO> queryWrapper = Wrappers.query(new ShortLinkDO())
                // 选择返回字段：分组ID(gid)和统计数量(shortLinkCount)
                .select("gid as gid, count(*) as shortLinkCount")
                // 条件1：分组ID在传入的参数列表中
                .in("gid", requestParam)
                // 条件2：启用状态为0（表示启用）
                .eq("enable_status", 0)
                // 条件3：删除标志为0（表示未删除）
                .eq("del_flag", 0)
                // 条件4：删除时间为0（表示未删除）
                .eq("del_time", 0L)
                // 按分组ID进行分组统计
                .groupBy("gid");

        // 执行查询操作
        // 说明：使用selectMaps获取结果集，返回Map列表（每个Map对应一行记录）
        List<Map<String, Object>> shortLinkDOList = baseMapper.selectMaps(queryWrapper);

        // 转换查询结果到响应DTO列表
        // 说明：使用BeanUtil工具类将Map列表转换为目标DTO列表
        // 注意：字段名会自动映射（gid->gid, shortLinkCount->shortLinkCount）
        return BeanUtil.copyToList(shortLinkDOList, ShortLinkGroupCountQueryRespDTO.class);
    }

    // 短链接还原方法（带异常抛出注解）
    @SneakyThrows
    @Override
    public void restoreUrl(String shortUri, ServletRequest request, ServletResponse response) {
        // 步骤1：构建完整短链接URL
        // 获取服务器名称（域名部分）
        String serverName = request.getServerName();
        // 处理端口号（非80端口时添加端口号）
        String serverPort = Optional.of(request.getServerPort())
                .filter(each -> !Objects.equals(each, 80))
                .map(String::valueOf)
                .map(each -> ":" + each)
                .orElse("");
        // 拼接完整短链接（域名+端口+短链接路径）
        String fullShortUrl = serverName + serverPort + "/" + shortUri;

        // 步骤2：尝试从Redis获取原始链接
        String originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
        // 情况1：缓存命中
        if (StrUtil.isNotBlank(originalLink)) {
            // 构建统计记录（包含用户信息）
            ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            // 记录访问统计（短链接维度）
            shortLinkStats(fullShortUrl, null, statsRecord);
            // 执行重定向到原始链接
            ((HttpServletResponse) response).sendRedirect(originalLink);
            return;
        }

        // 步骤3：布隆过滤器检查（防缓存穿透）
        boolean contains = shortUriCreateCachePenetrationBloomFilter.contains(fullShortUrl);
        // 情况2：布隆过滤器判断不存在
        if (!contains) {
            // 直接跳转到404页面
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }

        // 步骤4：检查空值缓存（防缓存击穿）
        String gotoIsNullShortLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl));
        // 情况3：存在空值标记
        if (StrUtil.isNotBlank(gotoIsNullShortLink)) {
            // 直接跳转到404页面
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }

        // 步骤5：获取分布式锁（防缓存击穿）
        RLock lock = redissonClient.getLock(String.format(LOCK_GOTO_SHORT_LINK_KEY, fullShortUrl));
        // 加锁（阻塞式）
        lock.lock();
        try {
            // 双重检查（获取锁后再次检查缓存）
            originalLink = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
            // 情况4：二次检查缓存命中
            if (StrUtil.isNotBlank(originalLink)) {
                // 构建统计记录
                ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
                // 记录访问统计
                shortLinkStats(fullShortUrl, null, statsRecord);
                // 执行重定向
                ((HttpServletResponse) response).sendRedirect(originalLink);
                return;
            }

            // 步骤6：查询路由表
            LambdaQueryWrapper<ShortLinkGotoDO> linkGotoQueryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                    .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
            ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(linkGotoQueryWrapper);
            // 情况5：路由表不存在记录
            if (shortLinkGotoDO == null) {
                // 设置空值缓存（30分钟过期）
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                // 跳转404页面
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }

            // 步骤7：查询短链接主表
            LambdaQueryWrapper<ShortLinkDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkDO.class)
                    .eq(ShortLinkDO::getGid, shortLinkGotoDO.getGid())
                    .eq(ShortLinkDO::getFullShortUrl, fullShortUrl)
                    .eq(ShortLinkDO::getDelFlag, 0)
                    .eq(ShortLinkDO::getEnableStatus, 0);
            ShortLinkDO shortLinkDO = baseMapper.selectOne(queryWrapper);
            // 情况6：记录不存在或已过期
            if (shortLinkDO == null || (shortLinkDO.getValidDate() != null && shortLinkDO.getValidDate().before(new Date()))) {
                // 设置空值缓存（30分钟过期）
                stringRedisTemplate.opsForValue().set(String.format(GOTO_IS_NULL_SHORT_LINK_KEY, fullShortUrl), "-", 30, TimeUnit.MINUTES);
                // 跳转404页面
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }

            // 步骤8：回填缓存
            stringRedisTemplate.opsForValue().set(
                    String.format(GOTO_SHORT_LINK_KEY, fullShortUrl),
                    shortLinkDO.getOriginUrl(),
                    LinkUtil.getLinkCacheValidTime(shortLinkDO.getValidDate()),
                    TimeUnit.MILLISECONDS
            );

            // 步骤9：记录完整统计信息（带分组ID）
            ShortLinkStatsRecordDTO statsRecord = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl, shortLinkDO.getGid(), statsRecord);

            // 最终重定向到原始链接
            ((HttpServletResponse) response).sendRedirect(shortLinkDO.getOriginUrl());
        } finally {
            // 确保锁释放
            lock.unlock();
        }
    }


    //========================
    //========================
    // 构建短链接统计记录并设置用户标识（UV）
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        // 原子布尔标记是否首次访问（UV维度）
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        // 获取请求中的Cookies
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        // 原子引用存储UV标识
        AtomicReference<String> uv = new AtomicReference<>();

        // 定义添加响应Cookie的任务（Lambda表达式）
        Runnable addResponseCookieTask = () -> {
            // 生成UUID作为UV标识
            uv.set(UUID.fastUUID().toString());
            // 创建UV Cookie
            Cookie uvCookie = new Cookie("uv", uv.get());
            // 设置Cookie有效期30天
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            // 设置Cookie路径（基于短链接路径）
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            // 添加Cookie到响应
            ((HttpServletResponse) response).addCookie(uvCookie);
            // 标记为首次访问
            uvFirstFlag.set(Boolean.TRUE);
            // 记录UV到Redis集合
            stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, uv.get());
        };

        // 处理现有Cookies
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    // 过滤出UV Cookie
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(
                            // 存在UV Cookie时的处理
                            each -> {
                                uv.set(each);
                                // 尝试添加UV到Redis集合（返回添加结果）
                                Long uvAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each);
                                // 根据添加结果判断是否首次访问
                                uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                            },
                            // 不存在UV Cookie时的处理（执行添加任务）
                            addResponseCookieTask
                    );
        } else {
            // 无Cookies时直接执行添加任务
            addResponseCookieTask.run();
        }

        // 获取客户端真实IP
        String remoteAddr = LinkUtil.getActualIp(((HttpServletRequest) request));
        // 获取客户端操作系统信息
        String os = LinkUtil.getOs(((HttpServletRequest) request));
        // 获取客户端浏览器信息
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        // 获取客户端设备信息
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        // 获取客户端网络类型
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));

        // 记录独立IP访问（UIP维度）
        Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, remoteAddr);
        // 判断是否首次IP访问
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;

        // 构建并返回统计记录DTO
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .build();
    }

        // 短链接访问统计实现方法
        @Override
            public void shortLinkStats(String fullShortUrl, String gid, ShortLinkStatsRecordDTO statsRecord) {
            // 处理fullShortUrl参数（优先使用参数值）
            fullShortUrl = Optional.ofNullable(fullShortUrl).orElse(statsRecord.getFullShortUrl());

            // 获取分布式读写锁（防止并发修改）
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(String.format(LOCK_GID_UPDATE_KEY, fullShortUrl));
            // 获取读锁
            RLock rLock = readWriteLock.readLock();

            // 尝试获取锁（非阻塞）
            if (!rLock.tryLock()) {
                // 获取锁失败时发送延迟统计消息
                delayShortLinkStatsProducer.send(statsRecord);
                return;
            }

            try {
                // 处理分组ID（gid）
                if (StrUtil.isBlank(gid)) {
                    // 查询路由表获取分组ID
                    LambdaQueryWrapper<ShortLinkGotoDO> queryWrapper = Wrappers.lambdaQuery(ShortLinkGotoDO.class)
                            .eq(ShortLinkGotoDO::getFullShortUrl, fullShortUrl);
                    ShortLinkGotoDO shortLinkGotoDO = shortLinkGotoMapper.selectOne(queryWrapper);
                    gid = shortLinkGotoDO.getGid();
                }

                // 获取当前时间信息
                int hour = DateUtil.hour(new Date(), true);         // 当前小时（24小时制）
                Week week = DateUtil.dayOfWeekEnum(new Date());     // 当前星期
                int weekValue = week.getIso8601Value();            // ISO标准星期值（1-7）

                // 构建基础访问统计记录
                LinkAccessStatsDO linkAccessStatsDO = LinkAccessStatsDO.builder()
                        .pv(1)                                      // 页面访问量+1
                        .uv(statsRecord.getUvFirstFlag() ? 1 : 0)  // 根据UV首次标记决定是否计数
                        .uip(statsRecord.getUipFirstFlag() ? 1 : 0) // 根据UIP首次标记决定是否计数
                        .hour(hour)                                 // 当前小时
                        .weekday(weekValue)                         // 当前星期
                        .fullShortUrl(fullShortUrl)                 // 完整短链接
                        .gid(gid)                                   // 分组ID
                        .date(new Date())                           // 当前日期
                        .build();
                // 写入基础访问统计
                linkAccessStatsMapper.shortLinkStats(linkAccessStatsDO);

                // 地理位置统计处理
                Map<String, Object> localeParamMap = new HashMap<>();
                localeParamMap.put("key", statsLocaleAmapKey);      // 高德地图API密钥
                localeParamMap.put("ip", statsRecord.getRemoteAddr()); // 客户端IP
                // 调用高德地图API获取地理位置
                String localeResultStr = HttpUtil.get(AMAP_REMOTE_URL, localeParamMap);
                JSONObject localeResultObj = JSON.parseObject(localeResultStr);
                String infoCode = localeResultObj.getString("infocode");

                // 默认地理位置值
                String actualProvince = "未知";
                String actualCity = "未知";

                // 成功获取地理位置时的处理
                if (StrUtil.isNotBlank(infoCode) && StrUtil.equals(infoCode, "10000")) {
                    String province = localeResultObj.getString("province");
                    boolean unknownFlag = StrUtil.equals(province, "[]");
                    // 构建地理位置统计记录
                    LinkLocaleStatsDO linkLocaleStatsDO = LinkLocaleStatsDO.builder()
                            .province(actualProvince = unknownFlag ? actualProvince : province)
                            .city(actualCity = unknownFlag ? actualCity : localeResultObj.getString("city"))
                            .adcode(unknownFlag ? "未知" : localeResultObj.getString("adcode"))
                            .cnt(1)                                 // 计数+1
                            .fullShortUrl(fullShortUrl)             // 完整短链接
                            .country("中国")                         // 默认国家
                            .gid(gid)                               // 分组ID
                            .date(new Date())                       // 当前日期
                            .build();
                    // 写入地理位置统计
                    linkLocaleStatsMapper.shortLinkLocaleState(linkLocaleStatsDO);
                }

                // 操作系统统计
                LinkOsStatsDO linkOsStatsDO = LinkOsStatsDO.builder()
                        .os(statsRecord.getOs())                    // 客户端操作系统
                        .cnt(1)                                     // 计数+1
                        .gid(gid)                                   // 分组ID
                        .fullShortUrl(fullShortUrl)                 // 完整短链接
                        .date(new Date())                           // 当前日期
                        .build();
                linkOsStatsMapper.shortLinkOsState(linkOsStatsDO);

                // 浏览器统计
                LinkBrowserStatsDO linkBrowserStatsDO = LinkBrowserStatsDO.builder()
                        .browser(statsRecord.getBrowser())          // 客户端浏览器
                        .cnt(1)                                      // 计数+1
                        .gid(gid)                                    // 分组ID
                        .fullShortUrl(fullShortUrl)                  // 完整短链接
                        .date(new Date())                            // 当前日期
                        .build();
                linkBrowserStatsMapper.shortLinkBrowserState(linkBrowserStatsDO);

                // 设备统计
                LinkDeviceStatsDO linkDeviceStatsDO = LinkDeviceStatsDO.builder()
                        .device(statsRecord.getDevice())             // 客户端设备
                        .cnt(1)                                      // 计数+1
                        .gid(gid)                                    // 分组ID
                        .fullShortUrl(fullShortUrl)                  // 完整短链接
                        .date(new Date())                            // 当前日期
                        .build();
                linkDeviceStatsMapper.shortLinkDeviceState(linkDeviceStatsDO);

                // 网络类型统计
                LinkNetworkStatsDO linkNetworkStatsDO = LinkNetworkStatsDO.builder()
                        .network(statsRecord.getNetwork())           // 客户端网络类型
                        .cnt(1)                                       // 计数+1
                        .gid(gid)                                     // 分组ID
                        .fullShortUrl(fullShortUrl)                   // 完整短链接
                        .date(new Date())                             // 当前日期
                        .build();
                linkNetworkStatsMapper.shortLinkNetworkState(linkNetworkStatsDO);

                // 访问日志记录
                LinkAccessLogsDO linkAccessLogsDO = LinkAccessLogsDO.builder()
                        .user(statsRecord.getUv())                    // UV标识
                        .ip(statsRecord.getRemoteAddr())              // 客户端IP
                        .browser(statsRecord.getBrowser())           // 浏览器
                        .os(statsRecord.getOs())                      // 操作系统
                        .network(statsRecord.getNetwork())             // 网络类型
                        .device(statsRecord.getDevice())               // 设备类型
                        .locale(StrUtil.join("-", "中国", actualProvince, actualCity)) // 地理位置
                        .gid(gid)                                     // 分组ID
                        .fullShortUrl(fullShortUrl)                    // 完整短链接
                        .build();
                linkAccessLogsMapper.insert(linkAccessLogsDO);

                // 更新基础统计计数器
                baseMapper.incrementStats(gid, fullShortUrl, 1, statsRecord.getUvFirstFlag() ? 1 : 0, statsRecord.getUipFirstFlag() ? 1 : 0);

                // 今日统计记录
                LinkStatsTodayDO linkStatsTodayDO = LinkStatsTodayDO.builder()
                        .todayPv(1)                                  // 今日PV+1
                        .todayUv(statsRecord.getUvFirstFlag() ? 1 : 0) // 根据UV标记决定今日UV是否+1
                        .todayUip(statsRecord.getUipFirstFlag() ? 1 : 0) // 根据UIP标记决定今日UIP是否+1
                        .gid(gid)                                    // 分组ID
                        .fullShortUrl(fullShortUrl)                  // 完整短链接
                        .date(new Date())                            // 当前日期
                        .build();
                linkStatsTodayMapper.shortLinkTodayState(linkStatsTodayDO);
            } catch (Throwable ex) {
                // 统计异常处理
                log.error("短链接访问量统计异常", ex);
            } finally {
                // 确保锁释放
                rLock.unlock();
            }
        }
    //================================
    //================================
    // 生成短链接后缀
    private String generateSuffix(ShortLinkCreateReqDTO requestParam) {
        int customGenerateCount = 0;
        String shorUri;
        while (true) {
            // 防止无限循环
            if (customGenerateCount > 10) {
                throw new ServiceException("短链接频繁生成，请稍后再试");
            }
            // 使用原始URL+时间戳生成哈希值
            String originUrl = requestParam.getOriginUrl();
            originUrl += System.currentTimeMillis();
            shorUri = HashUtil.hashToBase62(originUrl);
            // 检查布隆过滤器中是否已存在
            if (!shortUriCreateCachePenetrationBloomFilter.contains(createShortLinkDefaultDomain + "/" + shorUri)) {
                break;
            }
            customGenerateCount++;
        }
        return shorUri;
    }

    @SneakyThrows
    // 获取网站favicon图标
    private String getFavicon(String url) {
        URL targetUrl = new URL(url);
        HttpURLConnection connection = (HttpURLConnection) targetUrl.openConnection();
        connection.setRequestMethod("GET");
        connection.connect();
        int responseCode = connection.getResponseCode();
        if (HttpURLConnection.HTTP_OK == responseCode) {
            // 使用Jsoup解析HTML获取favicon链接
            Document document = Jsoup.connect(url).get();
            Element faviconLink = document.select("link[rel~=(?i)^(shortcut )?icon]").first();
            if (faviconLink != null) {
                return faviconLink.attr("abs:href");
            }
        }
        return null;
    }

    private void verificationWhitelist(String originUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if (enable == null || !enable) {
            return;
        }
        String domain = LinkUtil.extractDomain(originUrl);
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if (!details.contains(domain)) {
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }

}